package org.libtiff.jai.codecimpl;

/*
 * XTIFF: eXtensible TIFF libraries for JAI.
 * 
 * The contents of this file are subject to the JAVA ADVANCED IMAGING SAMPLE
 * INPUT-OUTPUT CODECS AND WIDGET HANDLING SOURCE CODE License Version 1.0 (the
 * "License"); You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.sun.com/software/imaging/JAI/index.html
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 * 
 * The Original Code is JAVA ADVANCED IMAGING SAMPLE INPUT-OUTPUT CODECS AND
 * WIDGET HANDLING SOURCE CODE. The Initial Developer of the Original Code is:
 * Sun Microsystems, Inc.. Portions created by: Niles Ritter are Copyright (C):
 * Niles Ritter, GeoTIFF.org, 1999,2000. All Rights Reserved. Contributor(s):
 * Niles Ritter
 */

import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Vector;

import org.libtiff.jai.codec.XTIFF;
import org.libtiff.jai.codec.XTIFFDirectory;
import org.libtiff.jai.codec.XTIFFEncodeParam;
import org.libtiff.jai.codec.XTIFFField;
import org.libtiff.jai.codec.XTIFFTileCodec;
import org.libtiff.jai.util.JaiI18N;

import com.sun.media.jai.codec.ImageEncodeParam;
import com.sun.media.jai.codec.TIFFEncodeParam;
import com.sun.media.jai.codec.TIFFField;
import com.sun.media.jai.codecimpl.TIFFImageDecoder;
import com.sun.media.jai.codecimpl.TIFFImageEncoder;

/**
 * A baseline TIFF writer. The writer outputs TIFF images in either Bilevel,
 * Greyscale, Palette color or Full Color modes.
 *  
 */
public class XTIFFImageEncoder extends TIFFImageEncoder
{

  long firstIFDOffset = 0;
  XTIFFDirectory directory;
  XTIFFEncodeParam tparam;
  int width;
  int length;
  SampleModel sampleModel;
  int numBands;
  int sampleSize[];
  int dataType;
  boolean dataTypeIsShort;
  ColorModel colorModel;
  int numTiles;
  int compression;
  boolean isTiled;
  long tileLength;
  long tileWidth;
  byte[] bpixels = null;
  long stripTileByteCounts[];
  long stripTileOffsets[];
  long currentOffset = 0;

  // Image Types
  public static final int XTIFF_BILEVEL_WHITE_IS_ZERO = 0;
  public static final int XTIFF_BILEVEL_BLACK_IS_ZERO = 1;
  public static final int XTIFF_PALETTE = 2;
  public static final int XTIFF_FULLCOLOR = 3;
  public static final int XTIFF_GREYSCALE = 4;

  /**
   * Standard constructor
   */
  public XTIFFImageEncoder(OutputStream output, ImageEncodeParam param)
  {
    super(output, param);
    if (this.param == null || !(param instanceof XTIFFEncodeParam))
    {
      this.param = new XTIFFEncodeParam((TIFFEncodeParam) param);
    }
    tparam = (XTIFFEncodeParam) this.param;
    directory = tparam.getDirectory();
  }

  private File createTemp() throws IOException
  {
    String tmpdir = System.getProperty("tiff.io.tmpdir");
    File file = null;
    if (tmpdir != null)
      file = File.createTempFile("libtiff.jai.", ".dat", new File(tmpdir));
    else
      file = File.createTempFile("libtiff.jai.", ".dat");
    file.deleteOnExit();
    return file;
  }

  private void copyImageData(File tmp, OutputStream out, int total)
      throws IOException
  {
    int bufsize = 1024;
    int bytes = 0;
    byte[] buf = new byte[bufsize];
    FileInputStream in = new FileInputStream(tmp);
    do
    {
      bytes = in.read(buf);
      out.write(buf, 0, bytes);
      total -= bytes;
    } while (total > 0);
    in.close();
  }

  /**
   * Encodes a RenderedImage and writes the output to the OutputStream
   * associated with this ImageEncoder.
   */
  public void encode(RenderedImage im) throws IOException
  {

    // Set comp into directory
    compression = tparam.getCompression();

    // see if tiled
    isTiled = ((TIFFEncodeParam) param).getWriteTiled();

    // Setup Directory fields.
    getImageFields(im);

    if (compression == XTIFF.COMPRESSION_NONE)
    {
      computeIFDOffset();
      writeFileHeader(firstIFDOffset);
      currentOffset = 8;
      writeImageData(im, output);
      writeDirectory(directory.getFields(), 0);
    } else
    {
      // We have to write compressed data out to
      // a temp file to compute the IFD offset.
      // The only alternative is to compress the
      // data twice, which is just about as bad.
      currentOffset = 8;
      File tmp = null;
      try
      {
        tmp = createTemp();
        OutputStream tmpOut = new FileOutputStream(tmp);
        int total = writeImageData(im, tmpOut);
        tmpOut.close();
        writeFileHeader(currentOffset + currentOffset % 2);
        copyImageData(tmp, output, total);
        writeDirectory(directory.getFields(), 0);
      } finally
      {
        if (tmp != null)
          tmp.delete();
      }
    }
  }

  /**
   * Precomputes the IFD Offset for uncompressed data.
   */
  private void computeIFDOffset()
  {

    long bytesPerRow = (long) Math.ceil((sampleSize[0] / 8.0) * tileWidth
        * numBands);
    long bytesPerTile = bytesPerRow * tileLength;
    long lastTile = bytesPerTile;

    if (!isTiled)
    {
      // Last strip may have lesser rows
      long lastStripRows = length - (tileLength * (numTiles - 1));
      lastTile = lastStripRows * bytesPerRow;
    }

    long totalBytesOfData = bytesPerTile * (numTiles - 1) + lastTile;

    // File header always occupies 8 bytes and we write the image data
    // after that.
    firstIFDOffset = 8 + totalBytesOfData;
    // Must begin on a word boundary
    if ((firstIFDOffset % 2) != 0)
    {
      firstIFDOffset++;
    }
  }

  private void writeFileHeader(long firstIFDOffset) throws IOException
  {
    // 8 byte image file header

    // Byte order used within the file - Big Endian
    output.write('M');
    output.write('M');

    // Magic value
    output.write(0);
    output.write(42);

    // Offset in bytes of the first IFD, must begin on a word boundary
    writeLong(firstIFDOffset);

  }

  // method for adding tags that haven't been set by user
  private void addIfAbsent(int tag, int type, int count, Object obj)
  {
    if (directory.getField(tag) == null)
      directory.addField(tag, type, count, obj);
  }

  private void getImageFields(RenderedImage im)
  /* throws IOException */
  {

    width = im.getWidth();
    length = im.getHeight(); //TIFF calls it length

    sampleModel = im.getSampleModel();
    numBands = sampleModel.getNumBands();
    sampleSize = sampleModel.getSampleSize();

    dataType = sampleModel.getDataType();
    if (dataType != DataBuffer.TYPE_BYTE && dataType != DataBuffer.TYPE_SHORT
        && dataType != DataBuffer.TYPE_USHORT)
    {
      // Support only byte and (unsigned) short.
      throw new Error(JaiI18N.getString("TIFFImageEncoder0"));
    }

    dataTypeIsShort = dataType == DataBuffer.TYPE_SHORT
        || dataType == DataBuffer.TYPE_USHORT;

    colorModel = im.getColorModel();
    if (colorModel != null && colorModel instanceof IndexColorModel
        && dataTypeIsShort)
    {
      // Don't support (unsigned) short palette-color images.
      throw new Error(JaiI18N.getString("TIFFImageEncoder2"));
    }
    IndexColorModel icm = null;
    int sizeOfColormap = 0;
    char colormap[] = null;

    // Basic fields - have to be in increasing numerical order BILEVEL
    // ImageWidth 256
    // ImageLength 257
    // BitsPerSample 258
    // Compression 259
    // PhotoMetricInterpretation 262
    // StripOffsets 273
    // RowsPerStrip 278
    // StripByteCounts 279
    // XResolution 282
    // YResolution 283
    // ResolutionUnit 296

    int photometricInterpretation = XTIFF.PHOTOMETRIC_RGB;
    int imageType = XTIFF_FULLCOLOR;

    // IMAGE TYPES POSSIBLE

    // Bilevel
    // BitsPerSample = 1
    // Compression = 1, 2, or 32773
    // PhotometricInterpretation either 0 or 1

    // Greyscale
    // BitsPerSample = 4 or 8
    // Compression = 1, 32773
    // PhotometricInterpretation either 0 or 1

    // Palette
    // ColorMap 320
    // BitsPerSample = 4 or 8
    // PhotometrciInterpretation = 3

    // Full color
    // BitsPerSample = 8, 8, 8
    // SamplesPerPixel = 3 or more 277
    // Compression = 1, 32773
    // PhotometricInterpretation = 2

    if (colorModel instanceof IndexColorModel)
    {

      icm = (IndexColorModel) colorModel;
      int mapSize = icm.getMapSize();

      if (sampleSize[0] == 1)
      {
        // Bilevel image

        if (mapSize != 2)
        {
          throw new IllegalArgumentException(JaiI18N
              .getString("TIFFImageEncoder1"));
        }

        byte r[] = new byte[mapSize];
        icm.getReds(r);
        byte g[] = new byte[mapSize];
        icm.getGreens(g);
        byte b[] = new byte[mapSize];
        icm.getBlues(b);

        if ((r[0] & 0xff) == 0 && (r[1] & 0xff) == 255 && (g[0] & 0xff) == 0
            && (g[1] & 0xff) == 255 && (b[0] & 0xff) == 0
            && (b[1] & 0xff) == 255)
        {

          imageType = XTIFF_BILEVEL_BLACK_IS_ZERO;

        } else if ((r[0] & 0xff) == 255 && (r[1] & 0xff) == 0
            && (g[0] & 0xff) == 255 && (g[1] & 0xff) == 0
            && (b[0] & 0xff) == 255 && (b[1] & 0xff) == 0)
        {

          imageType = XTIFF_BILEVEL_WHITE_IS_ZERO;

        } else
        {
          imageType = XTIFF_PALETTE;
        }

      } else
      {
        // Palette color image.
        imageType = XTIFF_PALETTE;
      }
    } else
    {

      // If it is not an IndexColorModel, it can either be a greyscale
      // image or a full color image

      if ((colorModel == null || colorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY)
          && numBands == 1)
      {
        // Greyscale image
        imageType = XTIFF_GREYSCALE;
      } else
      {
        // Full color image
        imageType = XTIFF_FULLCOLOR;
      }
    }

    switch (imageType)
    {

      case XTIFF_BILEVEL_WHITE_IS_ZERO :
        photometricInterpretation = XTIFF.PHOTOMETRIC_WHITE_IS_ZERO;
        break;

      case XTIFF_BILEVEL_BLACK_IS_ZERO :
        photometricInterpretation = XTIFF.PHOTOMETRIC_BLACK_IS_ZERO;
        break;

      case XTIFF_GREYSCALE :
        // Since the CS_GRAY colorspace is always of type black_is_zero
        photometricInterpretation = XTIFF.PHOTOMETRIC_BLACK_IS_ZERO;
        break;

      case XTIFF_PALETTE :
        photometricInterpretation = XTIFF.PHOTOMETRIC_PALETTE;

        icm = (IndexColorModel) colorModel;
        sizeOfColormap = icm.getMapSize();

        byte r[] = new byte[sizeOfColormap];
        icm.getReds(r);
        byte g[] = new byte[sizeOfColormap];
        icm.getGreens(g);
        byte b[] = new byte[sizeOfColormap];
        icm.getBlues(b);

        int redIndex = 0,
        greenIndex = sizeOfColormap;
        int blueIndex = 2 * sizeOfColormap;
        colormap = new char[sizeOfColormap * 3];
        for (int i = 0; i < sizeOfColormap; i++)
        {
          colormap[redIndex++] = (char) (r[i] << 8);
          colormap[greenIndex++] = (char) (g[i] << 8);
          colormap[blueIndex++] = (char) (b[i] << 8);
        }

        sizeOfColormap *= 3;

        // Since we will be writing the colormap field.
        break;

      case XTIFF_FULLCOLOR :
        photometricInterpretation = XTIFF.PHOTOMETRIC_RGB;
        break;

    }

    if (isTiled)
    {
      tileWidth = 16L;
      tileLength = 16L;
      XTIFFField fld = directory.getField(XTIFF.TIFFTAG_TILE_WIDTH);
      if (fld != null)
        tileWidth = (int) fld.getAsLong(0);
      fld = directory.getField(XTIFF.TIFFTAG_TILE_LENGTH);
      if (fld != null)
        tileLength = (int) fld.getAsLong(0);
    } else
    {
      // Default strip is 8 rows.
      tileLength = 8L;
      // tileWidth of strip is width

      tileWidth = width;
      XTIFFField fld = directory.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP);
      if (fld != null)
        tileLength = fld.getAsLong(0);
    }

    numTiles = (int) Math.ceil((double) length / (double) tileLength)
        * (int) Math.ceil((double) width / (double) tileWidth);

    stripTileByteCounts = new long[numTiles];
    stripTileOffsets = new long[numTiles];

    // Image Width
    directory.addField(XTIFF.TIFFTAG_IMAGE_WIDTH, TIFFField.TIFF_LONG, 1,
        (Object) (new long[]
        {width}));

    // Image Length
    directory.addField(XTIFF.TIFFTAG_IMAGE_LENGTH, TIFFField.TIFF_LONG, 1,
        new long[]
        {length});

    directory.addField(XTIFF.TIFFTAG_BITS_PER_SAMPLE, TIFFField.TIFF_SHORT,
        numBands, convertToChars(sampleSize));

    directory.addField(XTIFF.TIFFTAG_COMPRESSION, TIFFField.TIFF_SHORT, 1,
        new char[]
        {(char) compression});

    directory.addField(XTIFF.TIFFTAG_PHOTOMETRIC_INTERPRETATION,
        TIFFField.TIFF_SHORT, 1, new char[]
        {(char) photometricInterpretation});

    directory.addField(XTIFF.TIFFTAG_SAMPLES_PER_PIXEL, TIFFField.TIFF_SHORT,
        1, new char[]
        {(char) numBands});

    if (isTiled)
    {
      directory.addField(XTIFF.TIFFTAG_TILE_WIDTH, TIFFField.TIFF_LONG, 1,
          new long[]
          {tileWidth});

      directory.addField(XTIFF.TIFFTAG_TILE_LENGTH, TIFFField.TIFF_LONG, 1,
          new long[]
          {tileLength});

      directory.addField(XTIFF.TIFFTAG_TILE_OFFSETS, TIFFField.TIFF_LONG,
          numTiles, stripTileOffsets);

      directory.addField(XTIFF.TIFFTAG_TILE_BYTE_COUNTS, TIFFField.TIFF_LONG,
          numTiles, stripTileByteCounts);
    } else
    {
      directory.addField(XTIFF.TIFFTAG_STRIP_OFFSETS, TIFFField.TIFF_LONG,
          numTiles, stripTileOffsets);

      directory.addField(XTIFF.TIFFTAG_ROWS_PER_STRIP, TIFFField.TIFF_LONG, 1,
          new long[]
          {tileLength});

      directory.addField(XTIFF.TIFFTAG_STRIP_BYTE_COUNTS, TIFFField.TIFF_LONG,
          numTiles, stripTileByteCounts);
    }

    addIfAbsent(XTIFF.TIFFTAG_X_RESOLUTION, TIFFField.TIFF_RATIONAL, 1,
        new long[][]
        {
        {72, 1}});

    addIfAbsent(XTIFF.TIFFTAG_Y_RESOLUTION, TIFFField.TIFF_RATIONAL, 1,
        new long[][]
        {
        {72, 1}});

    addIfAbsent(XTIFF.TIFFTAG_RESOLUTION_UNIT, TIFFField.TIFF_SHORT, 1,
        new char[]
        {(char) 2});

    if (colormap != null)
    {
      directory.addField(XTIFF.TIFFTAG_COLORMAP, TIFFField.TIFF_SHORT,
          sizeOfColormap, colormap);
    }

    // Data Sample Format Extension fields.
    if (dataTypeIsShort)
    {
      // SampleFormat
      int[] sampleFormat = new int[numBands];
      sampleFormat[0] = dataType == DataBuffer.TYPE_USHORT ? 1 : 2;
      for (int b = 1; b < numBands; b++)
      {
        sampleFormat[b] = sampleFormat[0];
      }
      directory.addField(XTIFF.TIFFTAG_SAMPLE_FORMAT, TIFFField.TIFF_SHORT,
          numBands, convertToChars(sampleFormat));

      // SMinSampleValue: set to data type minimum.
      int[] minValue = new int[numBands];
      minValue[0] = dataType == DataBuffer.TYPE_USHORT ? 0 : Short.MIN_VALUE;
      for (int b = 1; b < numBands; b++)
      {
        minValue[b] = minValue[0];
      }
      directory.addField(XTIFF.TIFFTAG_S_MIN_SAMPLE_VALUE,
          TIFFField.TIFF_SHORT, numBands, convertToChars(minValue));

      // SMaxSampleValue: set to data type maximum.
      int[] maxValue = new int[numBands];
      maxValue[0] = dataType == DataBuffer.TYPE_USHORT
          ? 65535
          : Short.MAX_VALUE;
      for (int b = 1; b < numBands; b++)
      {
        maxValue[b] = maxValue[0];
      }
      directory.addField(XTIFF.TIFFTAG_S_MAX_SAMPLE_VALUE,
          TIFFField.TIFF_SHORT, numBands, convertToChars(maxValue));
    }

  }

  private char[] convertToChars(int[] shorts)
  {
    char[] out = new char[shorts.length];
    for (int i = 0; i < shorts.length; i++)
      out[i] = (char) shorts[i];
    return out;
  }

  protected int getSampleSize()
  {
    if (dataType == DataBuffer.TYPE_BYTE)
      return 1;
    else if (dataTypeIsShort)
      return 2;
    return 1; // what should go here?
  }

  protected int getTileSize()
  {
    return (int) (tileLength * tileWidth * numBands);
  }

  private int writeImageData(RenderedImage im, OutputStream out)
      throws IOException
  {
    int total = 0;

    // Get the encoder
    XTIFFTileCodec codec = directory.createTileCodec(tparam);

    // Create a buffer to hold the data
    // to be written to the file, so we can use array writes.
    int tsize = codec.getCompressedTileSize(im);
    bpixels = new byte[tsize];

    // Encode one tile at a time
    Rectangle rect = new Rectangle();
    float minX = (float) im.getMinX();
    float minY = (float) im.getMinY();
    float rows = (float) tileLength;
    float cols = (float) tileWidth;
    int i = 0;
    for (int row = 0; row < length; row += tileLength)
    {
      for (int col = 0; col < width; col += tileWidth)
      {
        if (!isTiled)
          rows = Math.min(tileLength, length - row);
        rect.setRect(minX + col, minY + row, cols, rows);
        int tileSize = codec.encode(im, rect, bpixels);
        out.write(bpixels, 0, tileSize);
        stripTileOffsets[i] = currentOffset;
        stripTileByteCounts[i++] = tileSize;
        currentOffset += tileSize;
        total += tileSize;
      }
    }
    return total;
  }

  private void writeDirectory(XTIFFField fields[], int nextIFDOffset)
      throws IOException
  {

    if (currentOffset % 2 == 1)
    {
      output.write(0);
      currentOffset++;
    }

    // 2 byte count of number of directory entries (fields)
    int numEntries = fields.length;

    long offsetBeyondIFD = currentOffset + 12 * numEntries + 4 + 2;
    Vector tooBig = new Vector();

    XTIFFField field;
    int tag;
    int type;
    int count;

    // Write number of fields in the IFD
    writeUnsignedShort(numEntries);

    for (int i = 0; i < numEntries; i++)
    {

      field = fields[i];

      // 12 byte field entry TIFFField

      // byte 0-1 Tag that identifies a field
      tag = field.getTag();
      writeUnsignedShort(tag);

      // byte 2-3 The field type
      type = field.getType();
      writeUnsignedShort(type);

      // bytes 4-7 the number of values of the indicated type
      count = field.getCount();
      writeLong(count);

      // bytes 8 - 11 the value offset
      if (count * sizeOfType[type] > 4)
      {

        // We need an offset as data won't fit into 4 bytes
        writeLong(offsetBeyondIFD);
        offsetBeyondIFD += (count * sizeOfType[type]);
        tooBig.add(new Integer(i));

      } else
      {

        writeValuesAsFourBytes(field);
      }

    }

    // Address of next IFD
    writeLong(nextIFDOffset);

    int index;
    // Write the tag values that did not fit into 4 bytes

    for (int i = 0; i < tooBig.size(); i++)
    {
      index = ((Integer) tooBig.elementAt(i)).intValue();
      writeValues(fields[index]);
    }
  }

  private static final int[] sizeOfType =
  {0, //  0 = n/a
      1, //  1 = byte
      1, //  2 = ascii
      2, //  3 = short
      4, //  4 = long
      8, //  5 = rational
      1, //  6 = sbyte
      1, //  7 = undefined
      2, //  8 = sshort
      4, //  9 = slong
      8, // 10 = srational
      4, // 11 = float
      8 // 12 = double
  };

  private void writeValuesAsFourBytes(XTIFFField field) throws IOException
  {

    int dataType = field.getType();
    int count = field.getCount();

    switch (dataType)
    {

      // unsigned 8 bits
      case TIFFField.TIFF_BYTE :
        byte bytes[] = field.getAsBytes();

        for (int i = 0; i < count; i++)
        {
          output.write(bytes[i]);
        }

        for (int i = 0; i < (4 - count); i++)
        {
          output.write(0);
        }

        break;

      // unsigned 16 bits
      case TIFFField.TIFF_SHORT :
        char shorts[] = field.getAsChars();

        for (int i = 0; i < count; i++)
        {
          writeUnsignedShort((int) shorts[i]);
        }

        for (int i = 0; i < (2 - count); i++)
        {
          writeUnsignedShort(0);
        }

        break;

      // unsigned 32 bits
      case TIFFField.TIFF_LONG :
        long longs[] = field.getAsLongs();

        for (int i = 0; i < count; i++)
        {
          writeLong(longs[i]);
        }
        break;
    }

  }

  private void writeValues(XTIFFField field) throws IOException
  {

    int dataType = field.getType();
    int count = field.getCount();

    switch (dataType)
    {

      // character data with NULL termination
      case TIFFField.TIFF_ASCII :
        String strings[] = field.getAsStrings();
        for (int i = 0; i < strings.length; i++)
        {
          byte bytes[] = strings[i].getBytes();
          for (int j = 0; j < bytes.length; j++)
          {
            output.write(bytes[j]);
          }
          if ((i + 1) < count)
            output.write(0);
        }
        break;

      // unsigned 8 bits
      case TIFFField.TIFF_BYTE :
        byte bytes[] = field.getAsBytes();
        for (int i = 0; i < count; i++)
        {
          output.write(bytes[i]);
        }
        break;

      // unsigned 16 bits
      case TIFFField.TIFF_SHORT :
        char shorts[] = field.getAsChars();
        for (int i = 0; i < count; i++)
        {
          writeUnsignedShort((int) shorts[i]);
        }
        break;

      // unsigned 32 bits
      case TIFFField.TIFF_LONG :
        long longs[] = field.getAsLongs();
        for (int i = 0; i < count; i++)
        {
          writeLong(longs[i]);
        }
        break;

      // IEEE 8-byte double
      case TIFFField.TIFF_DOUBLE :
        double doubles[] = field.getAsDoubles();
        for (int i = 0; i < count; i++)
        {
          writeDouble(doubles[i]);
        }
        break;

      case TIFFField.TIFF_RATIONAL :
        long rationals[][] = field.getAsRationals();
        for (int i = 0; i < count; i++)
        {
          writeLong(rationals[i][0]);
          writeLong(rationals[i][1]);
        }
        break;

    }

  }

  // Here s is never expected to have value greater than what can be
  // stored in 2 bytes.
  private void writeUnsignedShort(int s) throws IOException
  {
    output.write((s & 0xff00) >>> 8);
    output.write(s & 0x00ff);
  }

  private void writeLong(long l) throws IOException
  {
    output.write((int) ((l & 0xff000000) >>> 24));
    output.write((int) ((l & 0x00ff0000) >>> 16));
    output.write((int) ((l & 0x0000ff00) >>> 8));
    output.write(((int) l & 0x000000ff));
  }

  // write 8-byte IEEE double
  private void writeDouble(double d) throws IOException
  {
    long lval = Double.doubleToLongBits(d);
    writeLong(lval >>> 32);
    writeLong((lval & 0xffffffff));
  }

}

